// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported Workspace */
-const { Clutter, GLib, GObject, Meta, St } = imports.gi;
+const { Clutter, GLib, GObject, St } = imports.gi;
const DND = imports.ui.dnd;
const Main = imports.ui.main;
return start + (end - start) * step;
}
-var WindowPositionFlags = {
- NONE: 0,
- INITIAL: 1 << 0,
- ANIMATE: 1 << 1,
-};
-
// Window Thumbnail Layout Algorithm
// =================================
//
}
};
-function padArea(area, padding) {
- return {
- x: area.x + padding.left,
- y: area.y + padding.top,
- width: area.width - padding.left - padding.right,
- height: area.height - padding.top - padding.bottom,
- };
-}
-
-function rectEqual(one, two) {
- if (one == two)
- return true;
-
- if (!one || !two)
- return false;
-
- return one.x == two.x &&
- one.y == two.y &&
- one.width == two.width &&
- one.height == two.height;
-}
-
function animateAllocation(actor, box) {
if (actor.allocation.equal(box) ||
actor.allocation.get_width() === 0 ||
var Workspace = GObject.registerClass(
class Workspace extends St.Widget {
_init(metaWorkspace, monitorIndex) {
- super._init({ style_class: 'window-picker' });
+ super._init({
+ style_class: 'window-picker',
+ layout_manager: new WorkspaceLayout(metaWorkspace, monitorIndex),
+ });
// When dragging a window, we use this slot for reserve space.
this._reservedSlot = null;
this._reservedSlotWindow = null;
this.metaWorkspace = metaWorkspace;
- // The full geometry is the geometry we should try and position
- // windows for. The actual geometry we allocate may be less than
- // this, like if the workspace switcher is slid out.
- this._fullGeometry = null;
-
- // The actual geometry is the geometry we need to arrange windows
- // in. If this is a smaller area than the full geometry, we'll
- // do some simple aspect ratio like math to fit the layout calculated
- // for the full geometry into this area.
- this._actualGeometry = null;
- this._actualGeometryLater = 0;
-
- this._currentLayout = null;
-
this.monitorIndex = monitorIndex;
this._monitor = Main.layoutManager.monitors[this.monitorIndex];
if (monitorIndex != Main.layoutManager.primaryIndex)
this.add_style_class_name('external-monitor');
- this.set_size(0, 0);
-
- this._dropRect = new Clutter.Actor({ opacity: 0 });
- this._dropRect._delegate = this;
-
- this.add_actor(this._dropRect);
+ this.connect('style-changed', this._onStyleChanged.bind(this));
this.connect('destroy', this._onDestroy.bind(this));
const windows = global.get_window_actors().map(a => a.meta_window)
this._windows = [];
for (let i = 0; i < windows.length; i++) {
if (this._isOverviewWindow(windows[i]))
- this._addWindowClone(windows[i], true);
+ this._addWindowClone(windows[i]);
}
// Track window changes
this._windowEnteredMonitor.bind(this));
this._windowLeftMonitorId = global.display.connect('window-left-monitor',
this._windowLeftMonitor.bind(this));
- this._repositionWindowsId = 0;
-
- this.leavingOverview = false;
-
- this._positionWindowsFlags = 0;
- this._positionWindowsId = 0;
- }
+ this._layoutFrozenId = 0;
- vfunc_map() {
- super.vfunc_map();
- this._syncActualGeometry();
+ // DND requires this to be set
+ this._delegate = this;
}
vfunc_get_focus_chain() {
- return this.get_children().filter(c => c.visible).sort((a, b) => {
- if (a instanceof WindowPreview && b instanceof WindowPreview)
- return a.slotId - b.slotId;
-
- return 0;
- });
- }
-
- setFullGeometry(geom) {
- if (rectEqual(this._fullGeometry, geom))
- return;
-
- this._fullGeometry = geom;
-
- if (this.mapped)
- this._recalculateWindowPositions(WindowPositionFlags.NONE);
- }
-
- setActualGeometry(geom) {
- if (rectEqual(this._actualGeometry, geom))
- return;
-
- this._actualGeometry = geom;
- this._actualGeometryDirty = true;
-
- if (this.mapped)
- this._syncActualGeometry();
- }
-
- _syncActualGeometry() {
- if (this._actualGeometryLater || !this._actualGeometryDirty)
- return;
- if (!this._actualGeometry)
- return;
-
- this._actualGeometryLater = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
- this._actualGeometryLater = 0;
- if (!this.mapped)
- return false;
-
- let geom = this._actualGeometry;
-
- this._dropRect.set_position(geom.x, geom.y);
- this._dropRect.set_size(geom.width, geom.height);
- this._updateWindowPositions(Main.overview.animationInProgress ? WindowPositionFlags.ANIMATE : WindowPositionFlags.NONE);
-
- return false;
- });
+ return this.layout_manager.getFocusChain();
}
_lookupIndex(metaWindow) {
this._reservedSlotWindow = metaWindow;
this._reservedSlot = this._windows[this._lookupIndex(metaWindow)];
}
-
- this._recalculateWindowPositions(WindowPositionFlags.ANIMATE);
- }
-
- _recalculateWindowPositions(flags) {
- this._positionWindowsFlags |= flags;
-
- if (this._positionWindowsId > 0)
- return;
-
- this._positionWindowsId = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
- this._realRecalculateWindowPositions(this._positionWindowsFlags);
- this._positionWindowsFlags = 0;
- this._positionWindowsId = 0;
- return false;
- });
- }
-
- _realRecalculateWindowPositions(flags) {
- if (this._repositionWindowsId > 0) {
- GLib.source_remove(this._repositionWindowsId);
- this._repositionWindowsId = 0;
- }
-
- let clones = this._windows.slice();
- if (clones.length == 0)
- return;
-
- clones.sort((a, b) => {
- return a.metaWindow.get_stable_sequence() - b.metaWindow.get_stable_sequence();
- });
-
- if (this._reservedSlot)
- clones.push(this._reservedSlot);
-
- this._currentLayout = this._computeLayout(clones);
- this._updateWindowPositions(flags);
- }
-
- _updateWindowPositions(flags) {
- if (this._currentLayout == null) {
- this._recalculateWindowPositions(flags);
- return;
- }
-
- // We will reposition windows anyway when enter again overview or when ending the windows
- // animations with fade animation.
- // In this way we avoid unwanted animations of windows repositioning while
- // animating overview.
- if (this.leavingOverview || this._animatingWindowsFade)
- return;
-
- let initialPositioning = flags & WindowPositionFlags.INITIAL;
- let animate = flags & WindowPositionFlags.ANIMATE;
-
- let layout = this._currentLayout;
- let strategy = layout.strategy;
-
- let [, , padding] = this._getSpacingAndPadding();
- let area = padArea(this._actualGeometry, padding);
- let slots = strategy.computeWindowSlots(layout, area);
-
- const isOnCurrentWorkspace =
- this.metaWorkspace === null || this.metaWorkspace.active;
-
- for (let i = 0; i < slots.length; i++) {
- let slot = slots[i];
- const [x, y, cellWidth, cellHeight, clone] = slot;
-
- clone.slotId = i;
-
- // Positioning a window currently being dragged must be avoided;
- // we'll just leave a blank spot in the layout for it.
- if (clone.inDrag)
- continue;
-
- const cloneWidth = cellWidth;
- const cloneHeight = cellHeight;
-
- if (!clone.positioned) {
- // This window appeared after the overview was already up
- // Grow the clone from the center of the slot
- clone.x = x + cloneWidth / 2;
- clone.y = y + cloneHeight / 2;
- clone.width = 0;
- clone.height = 0;
- clone.positioned = true;
- }
-
- if (animate && isOnCurrentWorkspace) {
- if (!clone.metaWindow.showing_on_its_workspace()) {
- /* Hidden windows should fade in and grow
- * therefore we need to resize them now so they
- * can be scaled up later */
- if (initialPositioning) {
- clone.opacity = 0;
- clone.x = x;
- clone.y = y;
- clone.width = cloneWidth;
- clone.height = cloneHeight;
- }
-
- clone.ease({
- opacity: 255,
- mode: Clutter.AnimationMode.EASE_IN_QUAD,
- duration: Overview.ANIMATION_TIME,
- });
- }
-
- this._animateClone(clone, x, y, cloneWidth, cloneHeight);
- } else {
- // cancel any active tweens (otherwise they might override our changes)
- clone.remove_all_transitions();
- clone.set_position(x, y);
- clone.set_size(cloneWidth, cloneHeight);
- clone.set_opacity(255);
- }
- }
}
syncStacking(stackIndices) {
- let clones = this._windows.slice();
- clones.sort((a, b) => {
- let indexA = stackIndices[a.metaWindow.get_stable_sequence()];
- let indexB = stackIndices[b.metaWindow.get_stable_sequence()];
- return indexA - indexB;
- });
-
- for (let i = 0; i < clones.length; i++) {
- let clone = clones[i];
- if (i == 0) {
- clone.setStackAbove(this._dropRect);
- } else {
- let previousClone = clones[i - 1];
- clone.setStackAbove(previousClone);
- }
- }
- }
-
- _animateClone(clone, x, y, width, height) {
- clone.ease({
- x, y,
- width, height,
- duration: Overview.ANIMATION_TIME,
- mode: Clutter.AnimationMode.EASE_OUT_QUAD,
- });
- }
-
- _delayedWindowRepositioning() {
- let [x, y] = global.get_pointer();
-
- let pointerHasMoved = this._cursorX != x && this._cursorY != y;
- let inWorkspace = this._fullGeometry.x < x && x < this._fullGeometry.x + this._fullGeometry.width &&
- this._fullGeometry.y < y && y < this._fullGeometry.y + this._fullGeometry.height;
-
- if (pointerHasMoved && inWorkspace) {
- // store current cursor position
- this._cursorX = x;
- this._cursorY = y;
- return GLib.SOURCE_CONTINUE;
- }
-
- let actorUnderPointer = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y);
- for (let i = 0; i < this._windows.length; i++) {
- if (this._windows[i] == actorUnderPointer)
- return GLib.SOURCE_CONTINUE;
- }
-
- this._recalculateWindowPositions(WindowPositionFlags.ANIMATE);
- this._repositionWindowsId = 0;
- return GLib.SOURCE_REMOVE;
+ this.layout_manager.syncStacking(stackIndices);
}
_doRemoveWindow(metaWin) {
- let win = metaWin.get_compositor_private();
-
let clone = this._removeWindowClone(metaWin);
- if (clone) {
- // If metaWin.get_compositor_private() returned non-NULL, that
- // means the window still exists (and is just being moved to
- // another workspace or something), so set its overviewHint
- // accordingly. (If it returned NULL, then the window is being
- // destroyed; we'd like to animate this, but it's too late at
- // this point.)
- if (win) {
- let [stageX, stageY] = clone.get_transformed_position();
- const [transformedWidth, transformedHeight] =
- clone.get_transformed_size();
-
- metaWin._overviewHint = {
- x: stageX,
- y: stageY,
- width: transformedWidth,
- height: transformedHeight,
- };
- }
- clone.destroy();
- }
+ if (!clone)
+ return;
+
+ clone.destroy();
// We need to reposition the windows; to avoid shuffling windows
// around while the user is interacting with the workspace, we delay
// the positioning until the pointer remains still for at least 750 ms
// or is moved outside the workspace
+ this.layout_manager.layout_frozen = true;
- // remove old handler
- if (this._repositionWindowsId > 0) {
- GLib.source_remove(this._repositionWindowsId);
- this._repositionWindowsId = 0;
+ if (this._layoutFrozenId > 0) {
+ GLib.source_remove(this._layoutFrozenId);
+ this._layoutFrozenId = 0;
}
- // setup new handler
- let [x, y] = global.get_pointer();
- this._cursorX = x;
- this._cursorY = y;
+ let [oldX, oldY] = global.get_pointer();
+
+ this._layoutFrozenId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ WINDOW_REPOSITIONING_DELAY,
+ () => {
+ const [newX, newY] = global.get_pointer();
+ const pointerHasMoved = oldX !== newX || oldY !== newY;
+ const actorUnderPointer = global.stage.get_actor_at_pos(
+ Clutter.PickMode.REACTIVE, newX, newY);
+
+ if ((pointerHasMoved && this.contains(actorUnderPointer)) ||
+ this._windows.some(w => w.contains(actorUnderPointer))) {
+ oldX = newX;
+ oldY = newY;
+ return GLib.SOURCE_CONTINUE;
+ }
- this._currentLayout = null;
- this._repositionWindowsId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, WINDOW_REPOSITIONING_DELAY,
- this._delayedWindowRepositioning.bind(this));
- GLib.Source.set_name_by_id(this._repositionWindowsId, '[gnome-shell] this._delayedWindowRepositioning');
+ this.layout_manager.layout_frozen = false;
+ this._layoutFrozenId = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+
+ GLib.Source.set_name_by_id(this._layoutFrozenId,
+ '[gnome-shell] this._layoutFrozenId');
}
_doAddWindow(metaWin) {
- if (this.leavingOverview)
- return;
-
let win = metaWin.get_compositor_private();
if (!win) {
return;
}
- let clone = this._addWindowClone(metaWin, false);
-
- if (metaWin._overviewHint) {
- let x = metaWin._overviewHint.x - this.x;
- let y = metaWin._overviewHint.y - this.y;
- const width = metaWin._overviewHint.width;
- const height = metaWin._overviewHint.height;
- delete metaWin._overviewHint;
+ this._addWindowClone(metaWin);
- clone.positioned = true;
+ if (this._layoutFrozenId > 0) {
+ // If a window was closed before, unfreeze the layout to ensure
+ // the new window is immediately shown
+ this.layout_manager.layout_frozen = false;
- clone.set_position(x, y);
- clone.set_size(width, height);
+ GLib.source_remove(this._layoutFrozenId);
+ this._layoutFrozenId = 0;
}
-
- this._currentLayout = null;
- this._recalculateWindowPositions(WindowPositionFlags.ANIMATE);
}
_windowAdded(metaWorkspace, metaWin) {
fadeToOverview() {
// We don't want to reposition windows while animating in this way.
- this._animatingWindowsFade = true;
+ this.layout_manager.layout_frozen = true;
this._overviewShownId = Main.overview.connect('shown', this._doneShowingOverview.bind(this));
if (this._windows.length == 0)
return;
}
fadeFromOverview() {
- this.leavingOverview = true;
+ this.layout_manager.layout_frozen = true;
this._overviewHiddenId = Main.overview.connect('hidden', this._doneLeavingOverview.bind(this));
if (this._windows.length == 0)
return;
for (let i = 0; i < this._windows.length; i++)
this._windows[i].remove_all_transitions();
- if (this._repositionWindowsId > 0) {
- GLib.source_remove(this._repositionWindowsId);
- this._repositionWindowsId = 0;
+ if (this._layoutFrozenId > 0) {
+ GLib.source_remove(this._layoutFrozenId);
+ this._layoutFrozenId = 0;
}
if (this.metaWorkspace !== null && !this.metaWorkspace.active)
clone.hideOverlay(false);
if (clone.metaWindow.showing_on_its_workspace()) {
- clone.x = clone.boundingBox.x;
- clone.y = clone.boundingBox.y;
- clone.width = clone.boundingBox.width;
- clone.height = clone.boundingBox.height;
clone.ease({
opacity,
duration,
}
zoomToOverview() {
- // Position and scale the windows.
- this._recalculateWindowPositions(WindowPositionFlags.ANIMATE | WindowPositionFlags.INITIAL);
}
zoomFromOverview() {
- this.leavingOverview = true;
-
for (let i = 0; i < this._windows.length; i++)
this._windows[i].remove_all_transitions();
- if (this._repositionWindowsId > 0) {
- GLib.source_remove(this._repositionWindowsId);
- this._repositionWindowsId = 0;
+ if (this._layoutFrozenId > 0) {
+ GLib.source_remove(this._layoutFrozenId);
+ this._layoutFrozenId = 0;
}
+
+ this.layout_manager.layout_frozen = true;
this._overviewHiddenId = Main.overview.connect('hidden', this._doneLeavingOverview.bind(this));
if (this.metaWorkspace !== null && !this.metaWorkspace.active)
if (clone.metaWindow.showing_on_its_workspace()) {
clone.ease({
- x: clone.boundingBox.x,
- y: clone.boundingBox.y,
- width: clone.boundingBox.width,
- height: clone.boundingBox.height,
opacity: 255,
duration: Overview.ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
} else {
// The window is hidden, make it shrink and fade it out
clone.ease({
- width: 0,
- height: 0,
opacity: 0,
duration: Overview.ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
this._overviewHiddenId = 0;
}
+ if (this._overviewShownId) {
+ Main.overview.disconnect(this._overviewShownId);
+ this._overviewShownId = 0;
+ }
+
if (this.metaWorkspace) {
this.metaWorkspace.disconnect(this._windowAddedId);
this.metaWorkspace.disconnect(this._windowRemovedId);
global.display.disconnect(this._windowEnteredMonitorId);
global.display.disconnect(this._windowLeftMonitorId);
- if (this._repositionWindowsId > 0) {
- GLib.source_remove(this._repositionWindowsId);
- this._repositionWindowsId = 0;
- }
-
- if (this._positionWindowsId > 0) {
- Meta.later_remove(this._positionWindowsId);
- this._positionWindowsId = 0;
- }
-
- if (this._actualGeometryLater > 0) {
- Meta.later_remove(this._actualGeometryLater);
- this._actualGeometryLater = 0;
+ if (this._layoutFrozenId > 0) {
+ GLib.source_remove(this._layoutFrozenId);
+ this._layoutFrozenId = 0;
}
this._windows = [];
}
- // Sets this.leavingOverview flag to false.
_doneLeavingOverview() {
- this.leavingOverview = false;
+ this.layout_manager.layout_frozen = false;
}
_doneShowingOverview() {
- this._animatingWindowsFade = false;
- this._recalculateWindowPositions(WindowPositionFlags.INITIAL);
+ this.layout_manager.layout_frozen = false;
}
_isMyWindow(window) {
}
// Create a clone of a (non-desktop) window and add it to the window list
- _addWindowClone(metaWindow, positioned) {
+ _addWindowClone(metaWindow) {
let clone = new WindowPreview(metaWindow, this);
- clone.positioned = positioned;
clone.connect('selected',
this._onCloneSelected.bind(this));
clone.connect('drag-end', () => {
Main.overview.endWindowDrag(metaWindow);
});
- clone.connect('size-changed', () => {
- this._recalculateWindowPositions(WindowPositionFlags.NONE);
- });
clone.connect('show-chrome', () => {
let focus = global.stage.key_focus;
if (focus == null || this.contains(focus))
});
});
clone.connect('destroy', () => {
- this._removeWindowClone(metaWindow);
+ this._doRemoveWindow(metaWindow);
});
- this.add_child(clone);
+ this.layout_manager.addWindow(clone, metaWindow);
if (this._windows.length == 0)
clone.setStackAbove(null);
if (index == -1)
return null;
- return this._windows.splice(index, 1).pop();
- }
-
- _isBetterLayout(oldLayout, newLayout) {
- if (oldLayout.scale === undefined)
- return true;
-
- let spacePower = (newLayout.space - oldLayout.space) * LAYOUT_SPACE_WEIGHT;
- let scalePower = (newLayout.scale - oldLayout.scale) * LAYOUT_SCALE_WEIGHT;
-
- if (newLayout.scale > oldLayout.scale && newLayout.space > oldLayout.space) {
- // Win win -- better scale and better space
- return true;
- } else if (newLayout.scale > oldLayout.scale && newLayout.space <= oldLayout.space) {
- // Keep new layout only if scale gain outweighs aspect space loss
- return scalePower > spacePower;
- } else if (newLayout.scale <= oldLayout.scale && newLayout.space > oldLayout.space) {
- // Keep new layout only if aspect space gain outweighs scale loss
- return spacePower > scalePower;
- } else {
- // Lose -- worse scale and space
- return false;
- }
- }
-
- _getBestLayout(windows, area, rowSpacing, columnSpacing) {
- // We look for the largest scale that allows us to fit the
- // largest row/tallest column on the workspace.
-
- let lastLayout = {};
-
- let strategy = new UnalignedLayoutStrategy(this._monitor, rowSpacing, columnSpacing);
-
- for (let numRows = 1; ; numRows++) {
- let numColumns = Math.ceil(windows.length / numRows);
-
- // If adding a new row does not change column count just stop
- // (for instance: 9 windows, with 3 rows -> 3 columns, 4 rows ->
- // 3 columns as well => just use 3 rows then)
- if (numColumns == lastLayout.numColumns)
- break;
-
- let layout = { area, strategy, numRows, numColumns };
- strategy.computeLayout(windows, layout);
- strategy.computeScaleAndSpace(layout);
-
- if (!this._isBetterLayout(lastLayout, layout))
- break;
-
- lastLayout = layout;
- }
-
- return lastLayout;
- }
+ this.layout_manager.removeWindow(this._windows[index]);
- _getSpacingAndPadding() {
- let node = this.get_theme_node();
-
- // Window grid spacing
- let columnSpacing = node.get_length('-horizontal-spacing');
- let rowSpacing = node.get_length('-vertical-spacing');
- let padding = {
- left: node.get_padding(St.Side.LEFT),
- top: node.get_padding(St.Side.TOP),
- bottom: node.get_padding(St.Side.BOTTOM),
- right: node.get_padding(St.Side.RIGHT),
- };
-
- // All of the overlays have the same chrome sizes,
- // so just pick the first one.
- let clone = this._windows[0];
- let [topBorder, bottomBorder] = clone.chromeHeights();
- let [leftBorder, rightBorder] = clone.chromeWidths();
-
- rowSpacing += (topBorder + bottomBorder) / 2;
- columnSpacing += (rightBorder + leftBorder) / 2;
- padding.top += topBorder;
- padding.bottom += bottomBorder;
- padding.left += leftBorder;
- padding.right += rightBorder;
-
- return [rowSpacing, columnSpacing, padding];
+ return this._windows.splice(index, 1).pop();
}
- _computeLayout(windows) {
- let [rowSpacing, columnSpacing, padding] = this._getSpacingAndPadding();
- let area = padArea(this._fullGeometry, padding);
- return this._getBestLayout(windows, area, rowSpacing, columnSpacing);
+ _onStyleChanged() {
+ const themeNode = this.get_theme_node();
+ this.layout_manager.spacing = themeNode.get_length('spacing');
}
_onCloneSelected(clone, time) {
if (this._isMyWindow(window))
return false;
- // Set a hint on the Mutter.Window so its initial position
- // in the new workspace will be correct
- window._overviewHint = {
- x: actor.x,
- y: actor.y,
- width: actor.width,
- heigth: actor.height,
- };
-
// We need to move the window before changing the workspace, because
// the move itself could cause a workspace change if the window enters
// the primary monitor